#ifndef AT_CMD_H_INCLUDED
#define AT_CMD_H_INCLUDED

#include <stdbool.h>

// TODO: User-customizable error handling.
// TODO: (?) Asynch command handling?
// TODO: (?) Helper functions for parameter validation?

#ifndef UNUSED
#define UNUSED(X) ((void)(X))
#endif

/*
 *  This string stores the most recent error message for later use.
 */
extern char *at_last_error_message;

/*
 *  This is a more robust error-handling system that actively reports errors.
 *
 *  For finer-grained control of errors, making an error handler that conforms
 *  to the `at_error_handler_t` footprint and assigning that function to the
 *  global pointer `at_error_handler` will cause all errors to not only be
 *  stored in `at_last_error_message` but also to call back into user code with
 *  information about which file, line, and message is involved for better
 *  debugging.
 */
typedef void (*at_error_handler_t)(const char *file, const int line, const char *error_message);
extern at_error_handler_t at_error_handler;

/*
 *  Callbacks from the command processor must return true if successful and
 *  false otherwise.  If the command has a parameter, it will be passed as a
 *  string.  If there is no parameter, the passed-in string will be NULL.
 */
typedef bool (*at_static_callback_t)(const char *);

/*
 *  Commands are an array of command descriptors that include the text of the
 *  command (without the leading "+"), the callback function invoked when the
 *  command is executed, and any helpful text used to explain the command or
 *  list its permitted arguments, etc.
 *
 *  An array of these, terminated by a triple-null {NULL,NULL,NULL} is passed in
 *  to register the commands.  The helper macro AT_CMD_TERMINATOR is provided for
 *  ease of building registration tables.
 */
typedef struct at_static_descriptor_t
{
    char              *command;
    at_static_callback_t  callback;
    char              *help_text;
} at_static_descriptor_t;
#define AT_CMD_TERMINATOR {NULL,NULL,NULL}

/*
 *  Command sets are a set of `at_static_descriptor_t` records chained in a
 *  singly-linked list for linear searching.  Registration of new commands
 *  is done in the form of an `at_cmd_set_t`.
 *
 *  `at_cmd_set_t` entries should be in static or heap memory, not on the
 *  stack.
 */
typedef struct at_cmd_set_t
{
    const at_static_descriptor_t   *list;
    struct at_cmd_set_t         *next;
} at_cmd_set_t;

/*
 *  The following macros help ensure that command sets are constructed
 *  coherently.
 */

#define AT_CMD_BEGIN_SET(NAME) static at_static_descriptor_t _list_ ##NAME [] = {
#define AT_CMD_END_SET(NAME) } ; static at_cmd_set_t at_cmd_set_ ##NAME = { _list_ ##NAME, NULL, } ;
#define AT_CMD_SET(NAME) &at_cmd_set_ ##NAME

/*
 *  Registers a command set into the command system.  The command set must be
 *  in persistent memory space: ideally static, optionally unfreed heap.
 *
 *  Example of use:
 *
 *      AT_CMD_BEGIN_SET(modem_commands)
 *          { "BAUD", baud_handler, "sets baud rate, must be one of 300, 1200, 2400, 9600, 57600, or 115200" },
 *          { "MODE", mode_handler, "sets mode according to argument 'VERBOSE' or 'QUIET'" },
 *          AT_CMD_TERMINATOR,
 *      AT_CMD_END_SET(modem_commands)
 *      AT_Register(AT_CMD_SET(modem_commands));
 */
void AT_Register(at_cmd_set_t *);

/*
 *  Clean up all data associated with AT command processing.
 *
 *  Note: if multiple calls to AT_Register have been made all of them lose their
 *        data.
 */
void AT_DeRegister(void);

/*
 *  Process a line of text as an AT command.
 *
 *  An AT command:
 *    1. MUST start with the string "AT".
 *    2. MUST have a + before each command in the string.
 *    3. MAY have a parameter after the command separated by "=".
 *    4. MAY have multiple commands on the line separated by ";".
 *    5. MAY have whitespace at the beginning or end.
 *
 *  Examples:
 *    AT+HELP
 *    AT+LASTERROR
 *    AT+BAUD=2400;+FEC;+MODE=VERBOSE;+QUICK
 *
 *  Note: the last line has four commands: two with parameters, two without.
 */
bool AT_ProcessCommandLine(const char *);

/*******************************************************************************
* AT_CMD IMPLEMENTATION SECTION
* =============================
*
* In a single source file in your project, the symbol AT_CMD_IMPLEMENTATION must
* be defined before including the header:
*
*   ```c
*   #define AT_CMD_IMPLEMENTATION
*   #include "at_cmd.h"
*   ```
*
* All other source files using this library must *not* define this symbol.
*******************************************************************************/
#ifdef AT_CMD_IMPLEMENTATION

#include <ctype.h>
#include <stdlib.h>
#include <string.h>

// get around a problem where the people making GCC have been huffing glue
#define snprintf_nowarn(...) (snprintf(__VA_ARGS__) < 0 ? abort() : (void)0)

// default commands always available
static bool at_static_display_help(const char *);
static bool at_static_display_last_error(const char *);

// other helper functions
static bool at_static_execute_command(const char *, const char *);
static char *at_static_trim(const char *);

// macro to make output strings easier
#ifndef AT_OS
#include <stdio.h>
#define AT_OS(...) printf(__VA_ARGS__)
#endif

AT_CMD_BEGIN_SET(base_commands)
{ "HELP",      at_static_display_help,       "get help for command syntax"              },
{ "LASTERROR", at_static_display_last_error, "displays the last error message (if any)" },
AT_CMD_TERMINATOR, AT_CMD_END_SET(base_commands)

at_cmd_set_t *root = AT_CMD_SET(base_commands);

char *at_last_error_message;

// Error messages.
#define AT_ERR_FATAL_INITIALIZE    "could not initialize command system"
#define AT_ERR_ALLOC_FAILED        "could not allocate space for new commands"
#define AT_ERR_BAD_FORMAT_AT       "command line must begin with 'AT'"
#define AT_ERR_BAD_FORMAT_PLUS     "command must begin with '+'"
#define AT_ERR_UNKNOWN_CMD         "unknown command"
#define AT_ERR_HANDLER_FAILED      "command handler failed"
#define AT_ERR_NOT_REGISTERED      "no registered commands"

static char message_buffer[256];

void AT_Register(at_cmd_set_t *cmd_set)
{
    at_last_error_message = NULL;
    cmd_set->next = root;
    root = cmd_set;
}

bool AT_ProcessCommandLine(const char *line)
{
    bool retval = false;

    if (!root) {
        at_last_error_message = AT_ERR_NOT_REGISTERED;
        at_error_handler(__FILE__, __LINE__, at_last_error_message);
        return false;
    }

    char *dline = strdup(line), *wline = at_static_trim(dline);

    at_last_error_message = NULL;

    if (strncasecmp(wline, "AT", 2))
    {
        at_last_error_message = message_buffer;
        snprintf_nowarn(message_buffer, sizeof(message_buffer), "'%s': %s", wline, AT_ERR_BAD_FORMAT_AT);
        at_error_handler(__FILE__, __LINE__, at_last_error_message);
        goto abandon;
    }
    else
    {
        char *head, *tail;

        wline += 2;

        while (wline)
        {
            head = wline;

            if (*head++ != '+')
            {
                at_last_error_message = message_buffer;
                snprintf_nowarn(message_buffer, sizeof(message_buffer), "'%s': %s", head - 1, AT_ERR_BAD_FORMAT_PLUS);
                at_error_handler(__FILE__, __LINE__, at_last_error_message);
                goto abandon;
            }

            wline = strchr(wline, ';');

            if (wline)
            {
                *wline++ = '\000';
            }

            tail = strchr(head, '=');

            if (tail)
            {
                *tail++ = '\000';
            }

            if (!(retval = at_static_execute_command(head, tail)))
            {
                goto abandon;
            }
        }
    }

abandon:
    free(dline);
    return retval;
}

void AT_DeRegister(void)
{
    at_last_error_message = NULL;
    root = AT_CMD_SET(base_commands);
}

static bool at_static_display_help(const char *arg)
{
    AT_OS("FORMAT: AT+<command>[;+<command>...]\r\n"
       "Where:\r\n"
       "  <command>: <keyword>[=<value>]\r\n"
       "\r\nE.g.: AT+BAUD=2400;+FEC;+RUN\r\n"
       "\r\nAccepted commands:\r\n\n");
    at_cmd_set_t *head = root, *current;

    while (head)
    {
        current = head;
        head = head->next;
        const at_static_descriptor_t *commands = current->list;

        for (size_t i = 0; commands[i].command; i++)
        {
            AT_OS("%s -> %s\r\n", commands[i].command, commands[i].help_text);
        }
    }

    return true;
}

static bool at_static_display_last_error(const char *arg)
{
    AT_OS("%s\r\n", at_last_error_message ? at_last_error_message : "no error");
    at_last_error_message = NULL;

    return true;
}

static bool at_static_execute_command(const char *cmd, const char *prm)
{
    bool rv = false;

    at_cmd_set_t *head = root, *current;

    while (head)
    {
        current = head;
        head = head->next;
        const at_static_descriptor_t *commands = current->list;

        for (size_t i = 0; commands[i].command; i++)
        {
            if ((strlen(cmd) == strlen(commands[i].command))
                    && !(strcasecmp(cmd, commands[i].command)))
            {
                rv = commands[i].callback(prm);

                if (!rv)
                {
                    at_last_error_message = message_buffer;
                    snprintf_nowarn(message_buffer, sizeof(message_buffer), "'%s': %s", cmd, AT_ERR_HANDLER_FAILED);
                    at_error_handler(__FILE__, __LINE__, at_last_error_message);
                }

                goto abandon;
            }
        }
    }

    at_last_error_message = message_buffer;
    snprintf_nowarn(message_buffer, sizeof(message_buffer), "'%s': %s", cmd, AT_ERR_UNKNOWN_CMD);
    at_error_handler(__FILE__, __LINE__, at_last_error_message);

abandon:
    return rv;
}

static char *at_static_trim(const char *in)
{
    char *retval = (char *)in, *revp;

    while (isspace(*retval++)) {}

    retval--;
    revp = retval + strlen(retval) - 1;

    while (isspace(*revp))
    {
        *revp-- = '\000';
    }

    return retval;
}

/*******************************************************************************
* Static helper function definitions
*******************************************************************************/

void default_at_error_handler(const char *file, const int line, const char *error_message)
{
    UNUSED(file);
    UNUSED(line);
    UNUSED(error_message);
}
at_error_handler_t at_error_handler = default_at_error_handler;

#endif //AT_CMD_IMPLEMENTATION

#endif //AT_CMD_H_INCLUDED
